/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
 * Copyright (C) 2009 - 2014 Red Hat, Inc.
 * Copyright (C) 2009 Novell, Inc.
 */

#include "src/core/nm-default-daemon.h"

#include "nm-modem.h"

#include <fcntl.h>
#include <termios.h>
#include <linux/rtnetlink.h>

#include "libnm-core-intern/nm-core-internal.h"
#include "libnm-platform/nm-platform.h"
#include "nm-setting-connection.h"
#include "NetworkManagerUtils.h"
#include "devices/nm-device-private.h"
#include "nm-netns.h"
#include "nm-act-request.h"
#include "nm-ip4-config.h"
#include "nm-ip6-config.h"
#include "ppp/nm-ppp-manager-call.h"
#include "ppp/nm-ppp-status.h"

/*****************************************************************************/

NM_GOBJECT_PROPERTIES_DEFINE(NMModem,
                             PROP_CONTROL_PORT,
                             PROP_IP_IFINDEX,
                             PROP_PATH,
                             PROP_UID,
                             PROP_DRIVER,
                             PROP_STATE,
                             PROP_DEVICE_ID,
                             PROP_SIM_ID,
                             PROP_IP_TYPES,
                             PROP_SIM_OPERATOR_ID,
                             PROP_OPERATOR_CODE,
                             PROP_APN, );

enum {
    PPP_STATS,
    PPP_FAILED,
    PREPARE_RESULT,
    IP4_CONFIG_RESULT,
    IP6_CONFIG_RESULT,
    AUTH_REQUESTED,
    AUTH_RESULT,
    REMOVED,
    STATE_CHANGED,
    LAST_SIGNAL,
};

static guint signals[LAST_SIGNAL] = {0};

typedef struct _NMModemPrivate {
    char *uid;
    char *path;
    char *driver;
    char *control_port;
    char *data_port;

    /* TODO: ip_iface is solely used for nm_modem_owns_port().
     * We should rework the code that it's not necessary */
    char *ip_iface;

    int                ip_ifindex;
    NMModemIPMethod    ip4_method;
    NMModemIPMethod    ip6_method;
    NMUtilsIPv6IfaceId iid;
    NMModemState       state;
    NMModemState       prev_state; /* revert to this state if enable/disable fails */
    char *             device_id;
    char *             sim_id;
    NMModemIPType      ip_types;
    char *             sim_operator_id;
    char *             operator_code;
    char *             apn;

    NMPPPManager *ppp_manager;

    NMActRequest *                act_request;
    guint32                       secrets_tries;
    NMActRequestGetSecretsCallId *secrets_id;

    guint mm_ip_timeout;

    guint32 ip4_route_table;
    guint32 ip4_route_metric;
    guint32 ip6_route_table;
    guint32 ip6_route_metric;

    /* PPP stats */
    guint32 in_bytes;
    guint32 out_bytes;

    bool claimed : 1;
} NMModemPrivate;

G_DEFINE_TYPE(NMModem, nm_modem, G_TYPE_OBJECT)

#define NM_MODEM_GET_PRIVATE(self) _NM_GET_PRIVATE_PTR(self, NMModem, NM_IS_MODEM)

/*****************************************************************************/

#define _NMLOG_PREFIX_BUFLEN 64
#define _NMLOG_PREFIX_NAME   "modem"
#define _NMLOG_DOMAIN        LOGD_MB

static const char *
_nmlog_prefix(char *prefix, NMModem *self)
{
    const char *uuid;
    int         c;

    if (!self)
        return "";

    uuid = nm_modem_get_uid(self);

    if (uuid) {
        char pp[_NMLOG_PREFIX_BUFLEN - 5];

        c = g_snprintf(prefix, _NMLOG_PREFIX_BUFLEN, "[%s]", nm_strquote(pp, sizeof(pp), uuid));
    } else
        c = g_snprintf(prefix, _NMLOG_PREFIX_BUFLEN, "[%p]", self);
    nm_assert(c < _NMLOG_PREFIX_BUFLEN);

    return prefix;
}

#define _NMLOG(level, ...)                                                        \
    G_STMT_START                                                                  \
    {                                                                             \
        char _prefix[_NMLOG_PREFIX_BUFLEN];                                       \
                                                                                  \
        nm_log((level),                                                           \
               _NMLOG_DOMAIN,                                                     \
               NULL,                                                              \
               NULL,                                                              \
               "%s%s: " _NM_UTILS_MACRO_FIRST(__VA_ARGS__),                       \
               _NMLOG_PREFIX_NAME,                                                \
               _nmlog_prefix(_prefix, (self)) _NM_UTILS_MACRO_REST(__VA_ARGS__)); \
    }                                                                             \
    G_STMT_END

/*****************************************************************************/

static void _set_ip_ifindex(NMModem *self, int ifindex, const char *ifname);

/*****************************************************************************/
/* State/enabled/connected */

static const char *state_table[] = {
    [NM_MODEM_STATE_UNKNOWN]       = "unknown",
    [NM_MODEM_STATE_FAILED]        = "failed",
    [NM_MODEM_STATE_INITIALIZING]  = "initializing",
    [NM_MODEM_STATE_LOCKED]        = "locked",
    [NM_MODEM_STATE_DISABLED]      = "disabled",
    [NM_MODEM_STATE_DISABLING]     = "disabling",
    [NM_MODEM_STATE_ENABLING]      = "enabling",
    [NM_MODEM_STATE_ENABLED]       = "enabled",
    [NM_MODEM_STATE_SEARCHING]     = "searching",
    [NM_MODEM_STATE_REGISTERED]    = "registered",
    [NM_MODEM_STATE_DISCONNECTING] = "disconnecting",
    [NM_MODEM_STATE_CONNECTING]    = "connecting",
    [NM_MODEM_STATE_CONNECTED]     = "connected",
};

const char *
nm_modem_state_to_string(NMModemState state)
{
    if ((gsize) state < G_N_ELEMENTS(state_table))
        return state_table[state];
    return NULL;
}

/*****************************************************************************/

gboolean
nm_modem_is_claimed(NMModem *self)
{
    g_return_val_if_fail(NM_IS_MODEM(self), FALSE);

    return NM_MODEM_GET_PRIVATE(self)->claimed;
}

NMModem *
nm_modem_claim(NMModem *self)
{
    NMModemPrivate *priv;

    g_return_val_if_fail(NM_IS_MODEM(self), NULL);

    priv = NM_MODEM_GET_PRIVATE(self);

    g_return_val_if_fail(!priv->claimed, NULL);

    priv->claimed = TRUE;
    return g_object_ref(self);
}

void
nm_modem_unclaim(NMModem *self)
{
    NMModemPrivate *priv;

    g_return_if_fail(NM_IS_MODEM(self));

    priv = NM_MODEM_GET_PRIVATE(self);

    g_return_if_fail(priv->claimed);

    /* we don't actually unclaim the instance. This instance should not be re-used
     * by another owner, that is because we only claim modems as we receive them.
     * There is no mechanism that somebody else would later re-use them again.
     *
     * // priv->claimed = FALSE; */

    g_object_unref(self);
}

/*****************************************************************************/

NMModemState
nm_modem_get_state(NMModem *self)
{
    return NM_MODEM_GET_PRIVATE(self)->state;
}

void
nm_modem_set_state(NMModem *self, NMModemState new_state, const char *reason)
{
    NMModemPrivate *priv      = NM_MODEM_GET_PRIVATE(self);
    NMModemState    old_state = priv->state;

    priv->prev_state = NM_MODEM_STATE_UNKNOWN;

    if (new_state != old_state) {
        _LOGI("modem state changed, '%s' --> '%s' (reason: %s)",
              nm_modem_state_to_string(old_state),
              nm_modem_state_to_string(new_state),
              reason ?: "none");

        priv->state = new_state;
        _notify(self, PROP_STATE);
        g_signal_emit(self, signals[STATE_CHANGED], 0, (int) new_state, (int) old_state);
    }
}

void
nm_modem_set_prev_state(NMModem *self, const char *reason)
{
    NMModemPrivate *priv = NM_MODEM_GET_PRIVATE(self);

    /* Reset modem to previous state if the state hasn't already changed */
    if (priv->prev_state != NM_MODEM_STATE_UNKNOWN)
        nm_modem_set_state(self, priv->prev_state, reason);
}

void
nm_modem_set_mm_enabled(NMModem *self, gboolean enabled)
{
    NMModemPrivate *priv       = NM_MODEM_GET_PRIVATE(self);
    NMModemState    prev_state = priv->state;

    if (enabled && priv->state >= NM_MODEM_STATE_ENABLING) {
        _LOGD("cannot enable modem: already enabled");
        return;
    }
    if (!enabled && priv->state <= NM_MODEM_STATE_DISABLING) {
        _LOGD("cannot disable modem: already disabled");
        return;
    }

    if (priv->state <= NM_MODEM_STATE_INITIALIZING) {
        _LOGD("cannot enable/disable modem: initializing or failed");
        return;
    } else if (priv->state == NM_MODEM_STATE_LOCKED) {
        /* Don't try to enable if the modem is locked since that will fail */
        _LOGW("cannot enable/disable modem: locked");

        /* Try to unlock the modem if it's being enabled */
        if (enabled)
            g_signal_emit(self, signals[AUTH_REQUESTED], 0);
        return;
    }

    /* Not all modem classes support set_mm_enabled */
    if (NM_MODEM_GET_CLASS(self)->set_mm_enabled)
        NM_MODEM_GET_CLASS(self)->set_mm_enabled(self, enabled);

    /* Pre-empt the state change signal */
    nm_modem_set_state(self,
                       enabled ? NM_MODEM_STATE_ENABLING : NM_MODEM_STATE_DISABLING,
                       "user preference");
    priv->prev_state = prev_state;
}

void
nm_modem_emit_removed(NMModem *self)
{
    g_signal_emit(self, signals[REMOVED], 0);
}

void
nm_modem_emit_prepare_result(NMModem *self, gboolean success, NMDeviceStateReason reason)
{
    nm_assert(NM_IS_MODEM(self));

    g_signal_emit(self, signals[PREPARE_RESULT], 0, success, (guint) reason);
}

void
nm_modem_emit_ppp_failed(NMModem *self, NMDeviceStateReason reason)
{
    nm_assert(NM_IS_MODEM(self));

    g_signal_emit(self, signals[PPP_FAILED], 0, (guint) reason);
}

NMModemIPType
nm_modem_get_supported_ip_types(NMModem *self)
{
    return NM_MODEM_GET_PRIVATE(self)->ip_types;
}

const char *
nm_modem_ip_type_to_string(NMModemIPType ip_type)
{
    switch (ip_type) {
    case NM_MODEM_IP_TYPE_IPV4:
        return "ipv4";
    case NM_MODEM_IP_TYPE_IPV6:
        return "ipv6";
    case NM_MODEM_IP_TYPE_IPV4V6:
        return "ipv4v6";
    default:
        g_return_val_if_reached("unknown");
    }
}

static GArray *
build_single_ip_type_array(NMModemIPType type)
{
    return g_array_append_val(g_array_sized_new(FALSE, FALSE, sizeof(NMModemIPType), 1), type);
}

/**
 * nm_modem_get_connection_ip_type:
 * @self: the #NMModem
 * @connection: the #NMConnection to determine IP type to use
 *
 * Given a modem and a connection, determine which #NMModemIPTypes to use
 * when connecting.
 *
 * Returns: an array of #NMModemIpType values, in the order in which they
 * should be tried.
 */
GArray *
nm_modem_get_connection_ip_type(NMModem *self, NMConnection *connection, GError **error)
{
    NMModemPrivate *   priv = NM_MODEM_GET_PRIVATE(self);
    NMSettingIPConfig *s_ip4, *s_ip6;
    const char *       method;
    gboolean           ip4 = TRUE, ip6 = TRUE;
    gboolean           ip4_may_fail = TRUE, ip6_may_fail = TRUE;

    s_ip4 = nm_connection_get_setting_ip4_config(connection);
    if (s_ip4) {
        method = nm_setting_ip_config_get_method(s_ip4);
        if (g_strcmp0(method, NM_SETTING_IP4_CONFIG_METHOD_DISABLED) == 0)
            ip4 = FALSE;
        ip4_may_fail = nm_setting_ip_config_get_may_fail(s_ip4);
    }

    s_ip6 = nm_connection_get_setting_ip6_config(connection);
    if (s_ip6) {
        method = nm_setting_ip_config_get_method(s_ip6);
        if (NM_IN_STRSET(method,
                         NM_SETTING_IP6_CONFIG_METHOD_IGNORE,
                         NM_SETTING_IP6_CONFIG_METHOD_DISABLED))
            ip6 = FALSE;
        ip6_may_fail = nm_setting_ip_config_get_may_fail(s_ip6);
    }

    if (ip4 && !ip6) {
        if (!(priv->ip_types & NM_MODEM_IP_TYPE_IPV4)) {
            g_set_error_literal(error,
                                NM_DEVICE_ERROR,
                                NM_DEVICE_ERROR_INCOMPATIBLE_CONNECTION,
                                "Connection requested IPv4 but IPv4 is "
                                "unsupported by the modem.");
            return NULL;
        }
        return build_single_ip_type_array(NM_MODEM_IP_TYPE_IPV4);
    }

    if (ip6 && !ip4) {
        if (!(priv->ip_types & NM_MODEM_IP_TYPE_IPV6)) {
            g_set_error_literal(error,
                                NM_DEVICE_ERROR,
                                NM_DEVICE_ERROR_INCOMPATIBLE_CONNECTION,
                                "Connection requested IPv6 but IPv6 is "
                                "unsupported by the modem.");
            return NULL;
        }
        return build_single_ip_type_array(NM_MODEM_IP_TYPE_IPV6);
    }

    if (ip4 && ip6) {
        NMModemIPType type;
        GArray *      out;

        out = g_array_sized_new(FALSE, FALSE, sizeof(NMModemIPType), 3);

        /* Modem supports dual-stack? */
        if (priv->ip_types & NM_MODEM_IP_TYPE_IPV4V6) {
            type = NM_MODEM_IP_TYPE_IPV4V6;
            g_array_append_val(out, type);
        }

        /* If IPv6 may-fail=false, we should NOT try IPv4 as fallback */
        if ((priv->ip_types & NM_MODEM_IP_TYPE_IPV4) && ip6_may_fail) {
            type = NM_MODEM_IP_TYPE_IPV4;
            g_array_append_val(out, type);
        }

        /* If IPv4 may-fail=false, we should NOT try IPv6 as fallback */
        if ((priv->ip_types & NM_MODEM_IP_TYPE_IPV6) && ip4_may_fail) {
            type = NM_MODEM_IP_TYPE_IPV6;
            g_array_append_val(out, type);
        }

        if (out->len > 0)
            return out;

        /* Error... */
        g_array_unref(out);
        g_set_error_literal(error,
                            NM_DEVICE_ERROR,
                            NM_DEVICE_ERROR_INCOMPATIBLE_CONNECTION,
                            "Connection requested both IPv4 and IPv6 "
                            "but dual-stack addressing is unsupported "
                            "by the modem.");
        return NULL;
    }

    g_set_error_literal(error,
                        NM_DEVICE_ERROR,
                        NM_DEVICE_ERROR_INCOMPATIBLE_CONNECTION,
                        "Connection specified no IP configuration!");
    return NULL;
}

const char *
nm_modem_get_device_id(NMModem *self)
{
    return NM_MODEM_GET_PRIVATE(self)->device_id;
}

const char *
nm_modem_get_sim_id(NMModem *self)
{
    return NM_MODEM_GET_PRIVATE(self)->sim_id;
}

const char *
nm_modem_get_sim_operator_id(NMModem *self)
{
    return NM_MODEM_GET_PRIVATE(self)->sim_operator_id;
}

const char *
nm_modem_get_operator_code(NMModem *self)
{
    return NM_MODEM_GET_PRIVATE(self)->operator_code;
}

const char *
nm_modem_get_apn(NMModem *self)
{
    return NM_MODEM_GET_PRIVATE(self)->apn;
}

/*****************************************************************************/
/* IP method PPP */

static void
ppp_state_changed(NMPPPManager *ppp_manager, NMPPPStatus status, gpointer user_data)
{
    switch (status) {
    case NM_PPP_STATUS_DISCONNECT:
        nm_modem_emit_ppp_failed(user_data, NM_DEVICE_STATE_REASON_PPP_DISCONNECT);
        break;
    case NM_PPP_STATUS_DEAD:
        nm_modem_emit_ppp_failed(user_data, NM_DEVICE_STATE_REASON_PPP_FAILED);
        break;
    default:
        break;
    }
}

static void
ppp_ifindex_set(NMPPPManager *ppp_manager, int ifindex, const char *iface, gpointer user_data)
{
    NMModem *self = NM_MODEM(user_data);

    nm_assert(ifindex >= 0);
    nm_assert(NM_MODEM_GET_PRIVATE(self)->ppp_manager == ppp_manager);

    if (ifindex <= 0 && iface) {
        /* this might happen, if the ifname was already deleted
         * and we failed to resolve ifindex.
         *
         * Forget about the name. */
        iface = NULL;
    }
    _set_ip_ifindex(self, ifindex, iface);
}

static void
ppp_ip4_config(NMPPPManager *ppp_manager, NMIP4Config *config, gpointer user_data)
{
    NMModem *self = NM_MODEM(user_data);
    guint32  i, num;
    guint32  bad_dns1       = htonl(0x0A0B0C0D);
    guint32  good_dns1      = htonl(0x04020201); /* GTE nameserver */
    guint32  bad_dns2       = htonl(0x0A0B0C0E);
    guint32  good_dns2      = htonl(0x04020202); /* GTE nameserver */
    gboolean dns_workaround = FALSE;

    /* Work around a PPP bug (#1732) which causes many mobile broadband
     * providers to return 10.11.12.13 and 10.11.12.14 for the DNS servers.
     * Apparently fixed in ppp-2.4.5 but we've had some reports that this is
     * not the case.
     *
     * http://git.ozlabs.org/?p=ppp.git;a=commitdiff_plain;h=2e09ef6886bbf00bc5a9a641110f801e372ffde6
     * http://git.ozlabs.org/?p=ppp.git;a=commitdiff_plain;h=f8191bf07df374f119a07910a79217c7618f113e
     */

    num = nm_ip4_config_get_num_nameservers(config);
    if (num == 2) {
        gboolean found1 = FALSE, found2 = FALSE;

        for (i = 0; i < num; i++) {
            guint32 ns = nm_ip4_config_get_nameserver(config, i);

            if (ns == bad_dns1)
                found1 = TRUE;
            else if (ns == bad_dns2)
                found2 = TRUE;
        }

        /* Be somewhat conservative about substitutions; the "bad" nameservers
         * could actually be valid in some cases, so only substitute if ppp
         * returns *only* the two bad nameservers.
         */
        dns_workaround = (found1 && found2);
    }

    if (!num || dns_workaround) {
        _LOGW("compensating for invalid PPP-provided nameservers");
        nm_ip4_config_reset_nameservers(config);
        nm_ip4_config_add_nameserver(config, good_dns1);
        nm_ip4_config_add_nameserver(config, good_dns2);
    }

    g_signal_emit(self, signals[IP4_CONFIG_RESULT], 0, config, NULL);
}

static void
ppp_ip6_config(NMPPPManager *            ppp_manager,
               const NMUtilsIPv6IfaceId *iid,
               NMIP6Config *             config,
               gpointer                  user_data)
{
    NMModem *self = NM_MODEM(user_data);

    NM_MODEM_GET_PRIVATE(self)->iid = *iid;

    nm_modem_emit_ip6_config_result(self, config, NULL);
}

static void
ppp_stats(NMPPPManager *ppp_manager, guint i_in_bytes, guint i_out_bytes, gpointer user_data)
{
    NMModem *       self      = NM_MODEM(user_data);
    NMModemPrivate *priv      = NM_MODEM_GET_PRIVATE(self);
    guint32         in_bytes  = i_in_bytes;
    guint32         out_bytes = i_out_bytes;

    if (priv->in_bytes != in_bytes || priv->out_bytes != out_bytes) {
        priv->in_bytes  = in_bytes;
        priv->out_bytes = out_bytes;
        g_signal_emit(self, signals[PPP_STATS], 0, (guint) in_bytes, (guint) out_bytes);
    }
}

static gboolean
port_speed_is_zero(const char *port)
{
    struct termios    options;
    nm_auto_close int fd   = -1;
    gs_free char *    path = NULL;

    nm_assert(port);

    if (port[0] != '/') {
        if (!port[0] || strchr(port, '/') || NM_IN_STRSET(port, ".", ".."))
            return FALSE;
        path = g_build_path("/sys/class/tty", port, NULL);
        port = path;
    }

    fd = open(port, O_RDWR | O_NONBLOCK | O_NOCTTY | O_CLOEXEC);
    if (fd < 0)
        return FALSE;

    memset(&options, 0, sizeof(struct termios));
    if (tcgetattr(fd, &options) != 0)
        return FALSE;

    return cfgetospeed(&options) == B0;
}

static NMActStageReturn
ppp_stage3_ip_config_start(NMModem *            self,
                           NMActRequest *       req,
                           NMDeviceStateReason *out_failure_reason)
{
    NMModemPrivate *priv          = NM_MODEM_GET_PRIVATE(self);
    const char *    ppp_name      = NULL;
    GError *        error         = NULL;
    guint           ip_timeout    = 30;
    guint           baud_override = 0;

    g_return_val_if_fail(NM_IS_MODEM(self), NM_ACT_STAGE_RETURN_FAILURE);
    g_return_val_if_fail(NM_IS_ACT_REQUEST(req), NM_ACT_STAGE_RETURN_FAILURE);

    /* If we're already running PPP don't restart it; for example, if both
     * IPv4 and IPv6 are requested, IPv4 gets started first, but we use the
     * same pppd for both v4 and v6.
     */
    if (priv->ppp_manager)
        return NM_ACT_STAGE_RETURN_POSTPONE;

    if (NM_MODEM_GET_CLASS(self)->get_user_pass) {
        NMConnection *connection = nm_act_request_get_applied_connection(req);

        g_assert(connection);
        if (!NM_MODEM_GET_CLASS(self)->get_user_pass(self, connection, &ppp_name, NULL))
            return NM_ACT_STAGE_RETURN_FAILURE;
    }

    if (!priv->data_port) {
        _LOGE("error starting PPP (no data port)");
        NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_PPP_START_FAILED);
        return NM_ACT_STAGE_RETURN_FAILURE;
    }

    /* Check if ModemManager requested a specific IP timeout to be used. If 0 reported,
     * use the default one (30s) */
    if (priv->mm_ip_timeout > 0) {
        _LOGI("using modem-specified IP timeout: %u seconds", priv->mm_ip_timeout);
        ip_timeout = priv->mm_ip_timeout;
    }

    /* Some tty drivers and modems ignore port speed, but pppd requires the
     * port speed to be > 0 or it exits. If the port speed is 0 pass an
     * explicit speed to pppd to prevent the exit.
     * https://bugzilla.redhat.com/show_bug.cgi?id=1281731
     */
    if (port_speed_is_zero(priv->data_port))
        baud_override = 57600;

    priv->ppp_manager = nm_ppp_manager_create(priv->data_port, &error);

    if (priv->ppp_manager) {
        nm_ppp_manager_set_route_parameters(priv->ppp_manager,
                                            priv->ip4_route_table,
                                            priv->ip4_route_metric,
                                            priv->ip6_route_table,
                                            priv->ip6_route_metric);
    }

    if (!priv->ppp_manager
        || !nm_ppp_manager_start(priv->ppp_manager,
                                 req,
                                 ppp_name,
                                 ip_timeout,
                                 baud_override,
                                 &error)) {
        _LOGE("error starting PPP: %s", error->message);
        g_error_free(error);
        g_clear_object(&priv->ppp_manager);
        NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_PPP_START_FAILED);
        return NM_ACT_STAGE_RETURN_FAILURE;
    }

    g_signal_connect(priv->ppp_manager,
                     NM_PPP_MANAGER_SIGNAL_STATE_CHANGED,
                     G_CALLBACK(ppp_state_changed),
                     self);
    g_signal_connect(priv->ppp_manager,
                     NM_PPP_MANAGER_SIGNAL_IFINDEX_SET,
                     G_CALLBACK(ppp_ifindex_set),
                     self);
    g_signal_connect(priv->ppp_manager,
                     NM_PPP_MANAGER_SIGNAL_IP4_CONFIG,
                     G_CALLBACK(ppp_ip4_config),
                     self);
    g_signal_connect(priv->ppp_manager,
                     NM_PPP_MANAGER_SIGNAL_IP6_CONFIG,
                     G_CALLBACK(ppp_ip6_config),
                     self);
    g_signal_connect(priv->ppp_manager, NM_PPP_MANAGER_SIGNAL_STATS, G_CALLBACK(ppp_stats), self);

    return NM_ACT_STAGE_RETURN_POSTPONE;
}

/*****************************************************************************/

NMActStageReturn
nm_modem_stage3_ip4_config_start(NMModem *            self,
                                 NMDevice *           device,
                                 NMDeviceClass *      device_class,
                                 NMDeviceStateReason *out_failure_reason)
{
    NMModemPrivate * priv;
    NMActRequest *   req;
    NMConnection *   connection;
    const char *     method;
    NMActStageReturn ret;

    _LOGD("ip4_config_start");

    g_return_val_if_fail(NM_IS_MODEM(self), NM_ACT_STAGE_RETURN_FAILURE);
    g_return_val_if_fail(NM_IS_DEVICE(device), NM_ACT_STAGE_RETURN_FAILURE);
    g_return_val_if_fail(NM_IS_DEVICE_CLASS(device_class), NM_ACT_STAGE_RETURN_FAILURE);

    req = nm_device_get_act_request(device);
    g_return_val_if_fail(req, NM_ACT_STAGE_RETURN_FAILURE);

    connection = nm_act_request_get_applied_connection(req);
    g_return_val_if_fail(connection, NM_ACT_STAGE_RETURN_FAILURE);

    nm_modem_set_route_parameters_from_device(self, device);

    method = nm_utils_get_ip_config_method(connection, AF_INET);

    /* Only Disabled and Auto methods make sense for WWAN */
    if (nm_streq(method, NM_SETTING_IP4_CONFIG_METHOD_DISABLED))
        return NM_ACT_STAGE_RETURN_SUCCESS;

    if (!nm_streq(method, NM_SETTING_IP4_CONFIG_METHOD_AUTO)) {
        _LOGE("unhandled WWAN IPv4 method '%s'; will fail", method);
        NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_IP_METHOD_UNSUPPORTED);
        return NM_ACT_STAGE_RETURN_FAILURE;
    }

    priv = NM_MODEM_GET_PRIVATE(self);
    switch (priv->ip4_method) {
    case NM_MODEM_IP_METHOD_PPP:
        ret = ppp_stage3_ip_config_start(self, req, out_failure_reason);
        break;
    case NM_MODEM_IP_METHOD_STATIC:
        _LOGD("MODEM_IP_METHOD_STATIC");
        ret =
            NM_MODEM_GET_CLASS(self)->static_stage3_ip4_config_start(self, req, out_failure_reason);
        break;
    case NM_MODEM_IP_METHOD_AUTO:
        _LOGD("MODEM_IP_METHOD_AUTO");
        ret = device_class->act_stage3_ip_config_start(device, AF_INET, NULL, out_failure_reason);
        break;
    default:
        _LOGI("IPv4 configuration disabled");
        ret = NM_ACT_STAGE_RETURN_IP_FAIL;
        break;
    }

    return ret;
}

void
nm_modem_ip4_pre_commit(NMModem *modem, NMDevice *device, NMIP4Config *config)
{
    NMModemPrivate *priv = NM_MODEM_GET_PRIVATE(modem);

    /* If the modem has an ethernet-type data interface (ie, not PPP and thus
     * not point-to-point) and IP config has a /32 prefix, then we assume that
     * ARP will be pointless and we turn it off.
     */
    if (priv->ip4_method == NM_MODEM_IP_METHOD_STATIC
        || priv->ip4_method == NM_MODEM_IP_METHOD_AUTO) {
        const NMPlatformIP4Address *address = nm_ip4_config_get_first_address(config);

        g_assert(address);
        if (address->plen == 32)
            nm_platform_link_set_noarp(nm_device_get_platform(device),
                                       nm_device_get_ip_ifindex(device));
    }
}

/*****************************************************************************/

void
nm_modem_emit_ip6_config_result(NMModem *self, NMIP6Config *config, GError *error)
{
    NMModemPrivate *            priv = NM_MODEM_GET_PRIVATE(self);
    NMDedupMultiIter            ipconf_iter;
    const NMPlatformIP6Address *addr;
    gboolean                    do_slaac = TRUE;

    if (error) {
        g_signal_emit(self, signals[IP6_CONFIG_RESULT], 0, NULL, FALSE, error);
        return;
    }

    if (config) {
        /* If the IPv6 configuration only included a Link-Local address, then
         * we have to run SLAAC to get the full IPv6 configuration.
         */
        nm_ip_config_iter_ip6_address_for_each (&ipconf_iter, config, &addr) {
            if (IN6_IS_ADDR_LINKLOCAL(&addr->address)) {
                if (!priv->iid.id)
                    priv->iid.id = ((guint64 *) (&addr->address.s6_addr))[1];
            } else
                do_slaac = FALSE;
        }
    }
    g_assert(config || do_slaac);

    g_signal_emit(self, signals[IP6_CONFIG_RESULT], 0, config, do_slaac, NULL);
}

static NMActStageReturn
stage3_ip6_config_request(NMModem *self, NMDeviceStateReason *out_failure_reason)
{
    NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE);
    return NM_ACT_STAGE_RETURN_FAILURE;
}

NMActStageReturn
nm_modem_stage3_ip6_config_start(NMModem *            self,
                                 NMDevice *           device,
                                 NMDeviceStateReason *out_failure_reason)
{
    NMModemPrivate * priv;
    NMActRequest *   req;
    NMActStageReturn ret;
    NMConnection *   connection;
    const char *     method;

    g_return_val_if_fail(NM_IS_MODEM(self), NM_ACT_STAGE_RETURN_FAILURE);

    req = nm_device_get_act_request(device);
    g_return_val_if_fail(req, NM_ACT_STAGE_RETURN_FAILURE);

    connection = nm_act_request_get_applied_connection(req);
    g_return_val_if_fail(connection, NM_ACT_STAGE_RETURN_FAILURE);

    nm_modem_set_route_parameters_from_device(self, device);

    method = nm_utils_get_ip_config_method(connection, AF_INET6);

    /* Only Ignore, Disabled and Auto methods make sense for WWAN */
    if (NM_IN_STRSET(method,
                     NM_SETTING_IP6_CONFIG_METHOD_IGNORE,
                     NM_SETTING_IP6_CONFIG_METHOD_DISABLED))
        return NM_ACT_STAGE_RETURN_IP_DONE;

    if (!nm_streq(method, NM_SETTING_IP6_CONFIG_METHOD_AUTO)) {
        _LOGW("unhandled WWAN IPv6 method '%s'; will fail", method);
        NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_IP_CONFIG_UNAVAILABLE);
        return NM_ACT_STAGE_RETURN_FAILURE;
    }

    priv = NM_MODEM_GET_PRIVATE(self);
    switch (priv->ip6_method) {
    case NM_MODEM_IP_METHOD_PPP:
        ret = ppp_stage3_ip_config_start(self, req, out_failure_reason);
        break;
    case NM_MODEM_IP_METHOD_STATIC:
    case NM_MODEM_IP_METHOD_AUTO:
        /* Both static and DHCP/Auto retrieve a base IP config from the modem
         * which in the static case is the full config, and the DHCP/Auto case
         * is just the IPv6LL address to use for SLAAC.
         */
        ret = NM_MODEM_GET_CLASS(self)->stage3_ip6_config_request(self, out_failure_reason);
        break;
    default:
        _LOGI("IPv6 configuration disabled");
        ret = NM_ACT_STAGE_RETURN_IP_FAIL;
        break;
    }

    return ret;
}

guint32
nm_modem_get_configured_mtu(NMDevice *self, NMDeviceMtuSource *out_source, gboolean *out_force)
{
    NMConnection *connection;
    NMSetting *   setting;
    gint64        mtu_default;
    guint         mtu = 0;
    const char *  property_name;

    nm_assert(NM_IS_DEVICE(self));
    nm_assert(out_source);

    connection = nm_device_get_applied_connection(self);
    if (!connection)
        g_return_val_if_reached(0);

    setting = (NMSetting *) nm_connection_get_setting_gsm(connection);
    if (!setting)
        setting = (NMSetting *) nm_connection_get_setting_cdma(connection);

    if (setting) {
        g_object_get(setting, "mtu", &mtu, NULL);
        if (mtu) {
            *out_source = NM_DEVICE_MTU_SOURCE_CONNECTION;
            return mtu;
        }

        property_name = NM_IS_SETTING_GSM(setting) ? "gsm.mtu" : "cdma.mtu";
        mtu_default =
            nm_device_get_configured_mtu_from_connection_default(self, property_name, G_MAXUINT32);
        if (mtu_default >= 0) {
            *out_source = NM_DEVICE_MTU_SOURCE_CONNECTION;
            return (guint32) mtu_default;
        }
    }

    *out_source = NM_DEVICE_MTU_SOURCE_NONE;
    return 0;
}

/*****************************************************************************/

static void
cancel_get_secrets(NMModem *self)
{
    NMModemPrivate *priv = NM_MODEM_GET_PRIVATE(self);

    if (priv->secrets_id)
        nm_act_request_cancel_secrets(priv->act_request, priv->secrets_id);
}

static void
modem_secrets_cb(NMActRequest *                req,
                 NMActRequestGetSecretsCallId *call_id,
                 NMSettingsConnection *        connection,
                 GError *                      error,
                 gpointer                      user_data)
{
    NMModem *       self = NM_MODEM(user_data);
    NMModemPrivate *priv = NM_MODEM_GET_PRIVATE(self);

    g_return_if_fail(call_id == priv->secrets_id);

    priv->secrets_id = NULL;

    if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)
        || g_error_matches(error, NM_AGENT_MANAGER_ERROR, NM_AGENT_MANAGER_ERROR_NO_SECRETS))
        return;

    if (error)
        _LOGW("modem-secrets: %s", error->message);

    g_signal_emit(self, signals[AUTH_RESULT], 0, error);
}

void
nm_modem_get_secrets(NMModem *   self,
                     const char *setting_name,
                     gboolean    request_new,
                     const char *hint)
{
    NMModemPrivate *             priv  = NM_MODEM_GET_PRIVATE(self);
    NMSecretAgentGetSecretsFlags flags = NM_SECRET_AGENT_GET_SECRETS_FLAG_ALLOW_INTERACTION;

    cancel_get_secrets(self);

    if (request_new)
        flags |= NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW;
    priv->secrets_id = nm_act_request_get_secrets(priv->act_request,
                                                  FALSE,
                                                  setting_name,
                                                  flags,
                                                  NM_MAKE_STRV(hint),
                                                  modem_secrets_cb,
                                                  self);
    g_return_if_fail(priv->secrets_id);
    g_signal_emit(self, signals[AUTH_REQUESTED], 0);
}

/*****************************************************************************/

static NMActStageReturn
modem_act_stage1_prepare(NMModem *            modem,
                         NMConnection *       connection,
                         NMDeviceStateReason *out_failure_reason)
{
    NM_SET_OUT(out_failure_reason, NM_DEVICE_STATE_REASON_UNKNOWN);
    return NM_ACT_STAGE_RETURN_FAILURE;
}

NMActStageReturn
nm_modem_act_stage1_prepare(NMModem *            self,
                            NMActRequest *       req,
                            NMDeviceStateReason *out_failure_reason)
{
    NMModemPrivate *  priv                    = NM_MODEM_GET_PRIVATE(self);
    gs_unref_ptrarray GPtrArray *hints        = NULL;
    const char *                 setting_name = NULL;
    NMSecretAgentGetSecretsFlags flags        = NM_SECRET_AGENT_GET_SECRETS_FLAG_ALLOW_INTERACTION;
    NMConnection *               connection;

    g_return_val_if_fail(NM_IS_ACT_REQUEST(req), NM_ACT_STAGE_RETURN_FAILURE);

    if (priv->act_request)
        g_object_unref(priv->act_request);
    priv->act_request = g_object_ref(req);

    connection = nm_act_request_get_applied_connection(req);
    g_return_val_if_fail(connection, NM_ACT_STAGE_RETURN_FAILURE);

    setting_name = nm_connection_need_secrets(connection, &hints);
    if (!setting_name) {
        nm_assert(!hints);
        return NM_MODEM_GET_CLASS(self)->modem_act_stage1_prepare(self,
                                                                  connection,
                                                                  out_failure_reason);
    }

    /* Secrets required... */
    if (priv->secrets_tries++)
        flags |= NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW;

    if (hints)
        g_ptr_array_add(hints, NULL);

    priv->secrets_id = nm_act_request_get_secrets(req,
                                                  FALSE,
                                                  setting_name,
                                                  flags,
                                                  hints ? (const char *const *) hints->pdata : NULL,
                                                  modem_secrets_cb,
                                                  self);
    g_return_val_if_fail(priv->secrets_id, NM_ACT_STAGE_RETURN_FAILURE);
    g_signal_emit(self, signals[AUTH_REQUESTED], 0);
    return NM_ACT_STAGE_RETURN_POSTPONE;
}

/*****************************************************************************/

void
nm_modem_act_stage2_config(NMModem *self)
{
    NMModemPrivate *priv;

    g_return_if_fail(NM_IS_MODEM(self));

    priv = NM_MODEM_GET_PRIVATE(self);
    /* Clear secrets tries counter since secrets were successfully used
     * already if we get here.
     */
    priv->secrets_tries = 0;
}

/*****************************************************************************/

gboolean
nm_modem_check_connection_compatible(NMModem *self, NMConnection *connection, GError **error)
{
    NMModemPrivate *priv = NM_MODEM_GET_PRIVATE(self);

    if (nm_streq0(nm_connection_get_connection_type(connection), NM_SETTING_GSM_SETTING_NAME)) {
        NMSettingGsm *s_gsm;
        const char *  str;

        s_gsm = _nm_connection_check_main_setting(connection, NM_SETTING_GSM_SETTING_NAME, error);
        if (!s_gsm)
            return FALSE;

        str = nm_setting_gsm_get_device_id(s_gsm);
        if (str) {
            if (!priv->device_id) {
                nm_utils_error_set_literal(error,
                                           NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
                                           "GSM profile has device-id, device does not");
                return FALSE;
            }
            if (!nm_streq(str, priv->device_id)) {
                nm_utils_error_set_literal(error,
                                           NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
                                           "device has differing device-id than GSM profile");
                return FALSE;
            }
        }

        /* SIM properties may not be available before the SIM is unlocked, so
         * to ensure that autoconnect works, the connection's SIM properties
         * are only compared if present on the device.
         */

        if (priv->sim_id && (str = nm_setting_gsm_get_sim_id(s_gsm))) {
            if (!nm_streq(str, priv->sim_id)) {
                nm_utils_error_set_literal(error,
                                           NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
                                           "device has differing sim-id than GSM profile");
                return FALSE;
            }
        }

        if (priv->sim_operator_id && (str = nm_setting_gsm_get_sim_operator_id(s_gsm))) {
            if (!nm_streq(str, priv->sim_operator_id)) {
                nm_utils_error_set_literal(error,
                                           NM_UTILS_ERROR_CONNECTION_AVAILABLE_TEMPORARY,
                                           "device has differing sim-operator-id than GSM profile");
                return FALSE;
            }
        }
    }

    return NM_MODEM_GET_CLASS(self)->check_connection_compatible_with_modem(self,
                                                                            connection,
                                                                            error);
}

/*****************************************************************************/

gboolean
nm_modem_complete_connection(NMModem *            self,
                             const char *         iface,
                             NMConnection *       connection,
                             NMConnection *const *existing_connections,
                             GError **            error)
{
    NMModemClass *klass;

    klass = NM_MODEM_GET_CLASS(self);
    if (!klass->complete_connection) {
        g_set_error(error,
                    NM_DEVICE_ERROR,
                    NM_DEVICE_ERROR_INVALID_CONNECTION,
                    "Modem class %s had no complete_connection method",
                    G_OBJECT_TYPE_NAME(self));
        return FALSE;
    }

    return klass->complete_connection(self, iface, connection, existing_connections, error);
}

/*****************************************************************************/

static void
deactivate_cleanup(NMModem *self, NMDevice *device, gboolean stop_ppp_manager)
{
    NMModemPrivate *priv;
    int             ifindex;

    g_return_if_fail(NM_IS_MODEM(self));

    priv = NM_MODEM_GET_PRIVATE(self);

    priv->secrets_tries = 0;

    if (priv->act_request) {
        cancel_get_secrets(self);
        g_object_unref(priv->act_request);
        priv->act_request = NULL;
    }

    priv->in_bytes = priv->out_bytes = 0;

    if (priv->ppp_manager) {
        g_signal_handlers_disconnect_by_data(priv->ppp_manager, self);
        if (stop_ppp_manager)
            nm_ppp_manager_stop(priv->ppp_manager, NULL, NULL, NULL);
        g_clear_object(&priv->ppp_manager);
    }

    if (device) {
        g_return_if_fail(NM_IS_DEVICE(device));

        if (priv->ip4_method == NM_MODEM_IP_METHOD_STATIC
            || priv->ip4_method == NM_MODEM_IP_METHOD_AUTO
            || priv->ip6_method == NM_MODEM_IP_METHOD_STATIC
            || priv->ip6_method == NM_MODEM_IP_METHOD_AUTO) {
            ifindex = nm_device_get_ip_ifindex(device);
            if (ifindex > 0) {
                NMPlatform *platform = nm_device_get_platform(device);

                nm_platform_ip_route_flush(platform, AF_UNSPEC, ifindex);
                nm_platform_ip_address_flush(platform, AF_UNSPEC, ifindex);
                nm_platform_link_set_down(platform, ifindex);
            }
        }
    }

    nm_clear_g_free(&priv->data_port);
    priv->mm_ip_timeout = 0;
    priv->ip4_method    = NM_MODEM_IP_METHOD_UNKNOWN;
    priv->ip6_method    = NM_MODEM_IP_METHOD_UNKNOWN;
    _set_ip_ifindex(self, -1, NULL);
}

/*****************************************************************************/

typedef struct {
    NMModem *                 self;
    NMDevice *                device;
    GCancellable *            cancellable;
    NMModemDeactivateCallback callback;
    gpointer                  callback_user_data;
} DeactivateContext;

static void
deactivate_context_complete(DeactivateContext *ctx, GError *error)
{
    NMModem *self = ctx->self;

    _LOGD("modem deactivation finished %s%s%s",
          NM_PRINT_FMT_QUOTED(error, "with failure: ", error->message, "", "successfully"));

    if (ctx->callback)
        ctx->callback(ctx->self, error, ctx->callback_user_data);
    nm_g_object_unref(ctx->cancellable);
    g_object_unref(ctx->device);
    g_object_unref(ctx->self);
    g_slice_free(DeactivateContext, ctx);
}

static void
_deactivate_call_disconnect_cb(NMModem *self, GError *error, gpointer user_data)
{
    deactivate_context_complete(user_data, error);
}

static void
_deactivate_call_disconnect(DeactivateContext *ctx)
{
    NM_MODEM_GET_CLASS(ctx->self)->disconnect(ctx->self,
                                              FALSE,
                                              ctx->cancellable,
                                              _deactivate_call_disconnect_cb,
                                              ctx);
}

static void
_deactivate_ppp_manager_stop_cb(NMPPPManager *          ppp_manager,
                                NMPPPManagerStopHandle *handle,
                                gboolean                was_cancelled,
                                gpointer                user_data)
{
    DeactivateContext *ctx = user_data;

    g_object_unref(ppp_manager);

    if (was_cancelled) {
        gs_free_error GError *error = NULL;

        if (!g_cancellable_set_error_if_cancelled(ctx->cancellable, &error))
            nm_assert_not_reached();
        deactivate_context_complete(ctx, error);
        return;
    }

    nm_assert(!g_cancellable_is_cancelled(ctx->cancellable));
    _deactivate_call_disconnect(ctx);
}

void
nm_modem_deactivate_async(NMModem *                 self,
                          NMDevice *                device,
                          GCancellable *            cancellable,
                          NMModemDeactivateCallback callback,
                          gpointer                  user_data)
{
    NMModemPrivate *   priv = NM_MODEM_GET_PRIVATE(self);
    DeactivateContext *ctx;
    NMPPPManager *     ppp_manager;

    g_return_if_fail(NM_IS_MODEM(self));
    g_return_if_fail(NM_IS_DEVICE(device));
    g_return_if_fail(G_IS_CANCELLABLE(cancellable));

    ctx                     = g_slice_new(DeactivateContext);
    ctx->self               = g_object_ref(self);
    ctx->device             = g_object_ref(device);
    ctx->cancellable        = g_object_ref(cancellable);
    ctx->callback           = callback;
    ctx->callback_user_data = user_data;

    ppp_manager = nm_g_object_ref(priv->ppp_manager);

    NM_MODEM_GET_CLASS(self)->deactivate_cleanup(self, ctx->device, FALSE);

    if (ppp_manager) {
        /* If we have a PPP manager, stop it.
         *
         * Pass on the reference in @ppp_manager. */
        nm_ppp_manager_stop(ppp_manager, ctx->cancellable, _deactivate_ppp_manager_stop_cb, ctx);
        return;
    }

    _deactivate_call_disconnect(ctx);
}

/*****************************************************************************/

void
nm_modem_deactivate(NMModem *self, NMDevice *device)
{
    /* First cleanup */
    NM_MODEM_GET_CLASS(self)->deactivate_cleanup(self, device, TRUE);
    /* Then disconnect without waiting */
    NM_MODEM_GET_CLASS(self)->disconnect(self, FALSE, NULL, NULL, NULL);
}

/*****************************************************************************/

void
nm_modem_device_state_changed(NMModem *self, NMDeviceState new_state, NMDeviceState old_state)
{
    gboolean        was_connected = FALSE, warn = TRUE;
    NMModemPrivate *priv;

    g_return_if_fail(NM_IS_MODEM(self));

    if (old_state >= NM_DEVICE_STATE_PREPARE && old_state <= NM_DEVICE_STATE_DEACTIVATING)
        was_connected = TRUE;

    priv = NM_MODEM_GET_PRIVATE(self);

    /* Make sure we don't leave the serial device open */
    switch (new_state) {
    case NM_DEVICE_STATE_UNMANAGED:
    case NM_DEVICE_STATE_UNAVAILABLE:
    case NM_DEVICE_STATE_FAILED:
    case NM_DEVICE_STATE_DISCONNECTED:
        if (priv->act_request) {
            cancel_get_secrets(self);
            g_object_unref(priv->act_request);
            priv->act_request = NULL;
        }

        if (was_connected) {
            /* Don't bother warning on FAILED since the modem is already gone */
            if (new_state == NM_DEVICE_STATE_FAILED || new_state == NM_DEVICE_STATE_DISCONNECTED)
                warn = FALSE;
            /* First cleanup */
            NM_MODEM_GET_CLASS(self)->deactivate_cleanup(self, NULL, TRUE);
            NM_MODEM_GET_CLASS(self)->disconnect(self, warn, NULL, NULL, NULL);
        }
        break;
    default:
        break;
    }
}

/*****************************************************************************/

const char *
nm_modem_get_uid(NMModem *self)
{
    g_return_val_if_fail(NM_IS_MODEM(self), NULL);

    return NM_MODEM_GET_PRIVATE(self)->uid;
}

const char *
nm_modem_get_path(NMModem *self)
{
    g_return_val_if_fail(NM_IS_MODEM(self), NULL);

    return NM_MODEM_GET_PRIVATE(self)->path;
}

const char *
nm_modem_get_driver(NMModem *self)
{
    g_return_val_if_fail(NM_IS_MODEM(self), NULL);

    return NM_MODEM_GET_PRIVATE(self)->driver;
}

const char *
nm_modem_get_control_port(NMModem *self)
{
    g_return_val_if_fail(NM_IS_MODEM(self), NULL);

    return NM_MODEM_GET_PRIVATE(self)->control_port;
}

int
nm_modem_get_ip_ifindex(NMModem *self)
{
    NMModemPrivate *priv;

    g_return_val_if_fail(NM_IS_MODEM(self), 0);

    priv = NM_MODEM_GET_PRIVATE(self);

    /* internally we track an unset ip_ifindex as -1.
     * For the caller of nm_modem_get_ip_ifindex(), this
     * shall be zero too. */
    return priv->ip_ifindex != -1 ? priv->ip_ifindex : 0;
}

static void
_set_ip_ifindex(NMModem *self, int ifindex, const char *ifname)
{
    NMModemPrivate *priv = NM_MODEM_GET_PRIVATE(self);

    nm_assert(ifindex >= -1);
    nm_assert((ifindex > 0) == !!ifname);

    if (!nm_streq0(priv->ip_iface, ifname)) {
        g_free(priv->ip_iface);
        priv->ip_iface = g_strdup(ifname);
    }

    if (priv->ip_ifindex != ifindex) {
        priv->ip_ifindex = ifindex;
        _notify(self, PROP_IP_IFINDEX);
    }
}

gboolean
nm_modem_set_data_port(NMModem *       self,
                       NMPlatform *    platform,
                       const char *    data_port,
                       NMModemIPMethod ip4_method,
                       NMModemIPMethod ip6_method,
                       guint           timeout,
                       GError **       error)
{
    NMModemPrivate *priv;
    gboolean        is_ppp;
    int             ifindex = -1;

    g_return_val_if_fail(NM_IS_MODEM(self), FALSE);
    g_return_val_if_fail(NM_IS_PLATFORM(platform), FALSE);
    g_return_val_if_fail(!error || !*error, FALSE);

    priv = NM_MODEM_GET_PRIVATE(self);

    if (priv->ppp_manager || priv->data_port || priv->ip_ifindex != -1) {
        g_set_error_literal(error,
                            NM_UTILS_ERROR,
                            NM_UTILS_ERROR_UNKNOWN,
                            "cannot set data port in activated state");
        /* this really shouldn't happen. Assert. */
        g_return_val_if_reached(FALSE);
    }

    if (!data_port) {
        g_set_error_literal(error, NM_UTILS_ERROR, NM_UTILS_ERROR_UNKNOWN, "missing data port");
        return FALSE;
    }

    is_ppp = (ip4_method == NM_MODEM_IP_METHOD_PPP) || (ip6_method == NM_MODEM_IP_METHOD_PPP);
    if (is_ppp) {
        if (!NM_IN_SET(ip4_method, NM_MODEM_IP_METHOD_UNKNOWN, NM_MODEM_IP_METHOD_PPP)
            || !NM_IN_SET(ip6_method, NM_MODEM_IP_METHOD_UNKNOWN, NM_MODEM_IP_METHOD_PPP)) {
            g_set_error_literal(error,
                                NM_UTILS_ERROR,
                                NM_UTILS_ERROR_UNKNOWN,
                                "conflicting ip methods");
            return FALSE;
        }
    } else if (!NM_IN_SET(ip4_method,
                          NM_MODEM_IP_METHOD_UNKNOWN,
                          NM_MODEM_IP_METHOD_STATIC,
                          NM_MODEM_IP_METHOD_AUTO)
               || !NM_IN_SET(ip6_method,
                             NM_MODEM_IP_METHOD_UNKNOWN,
                             NM_MODEM_IP_METHOD_STATIC,
                             NM_MODEM_IP_METHOD_AUTO)
               || (ip4_method == NM_MODEM_IP_METHOD_UNKNOWN
                   && ip6_method == NM_MODEM_IP_METHOD_UNKNOWN)) {
        g_set_error_literal(error, NM_UTILS_ERROR, NM_UTILS_ERROR_UNKNOWN, "invalid ip methods");
        return FALSE;
    }

    if (!is_ppp) {
        ifindex = nm_platform_if_nametoindex(platform, data_port);
        if (ifindex <= 0) {
            g_set_error(error,
                        NM_UTILS_ERROR,
                        NM_UTILS_ERROR_UNKNOWN,
                        "cannot find network interface %s",
                        data_port);
            return FALSE;
        }
        if (!nm_platform_process_events_ensure_link(platform, ifindex, data_port)) {
            g_set_error(error,
                        NM_UTILS_ERROR,
                        NM_UTILS_ERROR_UNKNOWN,
                        "cannot find network interface %s in platform cache",
                        data_port);
            return FALSE;
        }
    }

    priv->mm_ip_timeout = timeout;
    priv->ip4_method    = ip4_method;
    priv->ip6_method    = ip6_method;
    if (is_ppp) {
        priv->data_port = g_strdup(data_port);
        _set_ip_ifindex(self, -1, NULL);
    } else {
        priv->data_port = NULL;
        _set_ip_ifindex(self, ifindex, data_port);
    }
    return TRUE;
}

gboolean
nm_modem_owns_port(NMModem *self, const char *iface)
{
    NMModemPrivate *priv = NM_MODEM_GET_PRIVATE(self);

    g_return_val_if_fail(iface != NULL, FALSE);

    if (NM_MODEM_GET_CLASS(self)->owns_port)
        return NM_MODEM_GET_CLASS(self)->owns_port(self, iface);

    return NM_IN_STRSET(iface, priv->ip_iface, priv->data_port, priv->control_port);
}

gboolean
nm_modem_get_iid(NMModem *self, NMUtilsIPv6IfaceId *out_iid)
{
    g_return_val_if_fail(NM_IS_MODEM(self), FALSE);

    *out_iid = NM_MODEM_GET_PRIVATE(self)->iid;
    return TRUE;
}

/*****************************************************************************/

void
nm_modem_get_route_parameters(NMModem *self,
                              guint32 *out_ip4_route_table,
                              guint32 *out_ip4_route_metric,
                              guint32 *out_ip6_route_table,
                              guint32 *out_ip6_route_metric)
{
    NMModemPrivate *priv;

    g_return_if_fail(NM_IS_MODEM(self));

    priv = NM_MODEM_GET_PRIVATE(self);
    NM_SET_OUT(out_ip4_route_table, priv->ip4_route_table);
    NM_SET_OUT(out_ip4_route_metric, priv->ip4_route_metric);
    NM_SET_OUT(out_ip6_route_table, priv->ip6_route_table);
    NM_SET_OUT(out_ip6_route_metric, priv->ip6_route_metric);
}

void
nm_modem_set_route_parameters(NMModem *self,
                              guint32  ip4_route_table,
                              guint32  ip4_route_metric,
                              guint32  ip6_route_table,
                              guint32  ip6_route_metric)
{
    NMModemPrivate *priv;

    g_return_if_fail(NM_IS_MODEM(self));

    priv = NM_MODEM_GET_PRIVATE(self);
    if (priv->ip4_route_table != ip4_route_table || priv->ip4_route_metric != ip4_route_metric
        || priv->ip6_route_table != ip6_route_table || priv->ip6_route_metric != ip6_route_metric) {
        priv->ip4_route_table  = ip4_route_table;
        priv->ip4_route_metric = ip4_route_metric;
        priv->ip6_route_table  = ip6_route_table;
        priv->ip6_route_metric = ip6_route_metric;

        _LOGT("route-parameters: table-v4: %u, metric-v4: %u, table-v6: %u, metric-v6: %u",
              priv->ip4_route_table,
              priv->ip4_route_metric,
              priv->ip6_route_table,
              priv->ip6_route_metric);
    }

    if (priv->ppp_manager) {
        nm_ppp_manager_set_route_parameters(priv->ppp_manager,
                                            priv->ip4_route_table,
                                            priv->ip4_route_metric,
                                            priv->ip6_route_table,
                                            priv->ip6_route_metric);
    }
}

void
nm_modem_set_route_parameters_from_device(NMModem *self, NMDevice *device)
{
    g_return_if_fail(NM_IS_DEVICE(device));

    nm_modem_set_route_parameters(self,
                                  nm_device_get_route_table(device, AF_INET),
                                  nm_device_get_route_metric(device, AF_INET),
                                  nm_device_get_route_table(device, AF_INET6),
                                  nm_device_get_route_metric(device, AF_INET6));
}

/*****************************************************************************/

void
nm_modem_get_capabilities(NMModem *                  self,
                          NMDeviceModemCapabilities *modem_caps,
                          NMDeviceModemCapabilities *current_caps)
{
    g_return_if_fail(NM_IS_MODEM(self));

    NM_MODEM_GET_CLASS(self)->get_capabilities(self, modem_caps, current_caps);
}

/*****************************************************************************/

void
_nm_modem_set_operator_code(NMModem *self, const char *operator_code)
{
    NMModemPrivate *priv = NM_MODEM_GET_PRIVATE(self);

    if (g_strcmp0(priv->operator_code, operator_code) != 0) {
        g_free(priv->operator_code);
        priv->operator_code = g_strdup(operator_code);
        _notify(self, PROP_OPERATOR_CODE);
    }
}

void
_nm_modem_set_apn(NMModem *self, const char *apn)
{
    NMModemPrivate *priv = NM_MODEM_GET_PRIVATE(self);

    if (g_strcmp0(priv->apn, apn) != 0) {
        g_free(priv->apn);
        priv->apn = g_strdup(apn);
        _notify(self, PROP_APN);
    }
}

static void
get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
{
    NMModem *       self = NM_MODEM(object);
    NMModemPrivate *priv = NM_MODEM_GET_PRIVATE(self);

    switch (prop_id) {
    case PROP_PATH:
        g_value_set_string(value, priv->path);
        break;
    case PROP_DRIVER:
        g_value_set_string(value, priv->driver);
        break;
    case PROP_CONTROL_PORT:
        g_value_set_string(value, priv->control_port);
        break;
    case PROP_IP_IFINDEX:
        g_value_set_int(value, nm_modem_get_ip_ifindex(self));
        break;
    case PROP_UID:
        g_value_set_string(value, priv->uid);
        break;
    case PROP_STATE:
        g_value_set_int(value, priv->state);
        break;
    case PROP_DEVICE_ID:
        g_value_set_string(value, priv->device_id);
        break;
    case PROP_SIM_ID:
        g_value_set_string(value, priv->sim_id);
        break;
    case PROP_IP_TYPES:
        g_value_set_uint(value, priv->ip_types);
        break;
    case PROP_SIM_OPERATOR_ID:
        g_value_set_string(value, priv->sim_operator_id);
        break;
    case PROP_OPERATOR_CODE:
        g_value_set_string(value, priv->operator_code);
        break;
    case PROP_APN:
        g_value_set_string(value, priv->apn);
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
        break;
    }
}

static void
set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
{
    NMModemPrivate *priv = NM_MODEM_GET_PRIVATE(object);
    const char *    s;

    switch (prop_id) {
    case PROP_PATH:
        /* construct-only */
        priv->path = g_value_dup_string(value);
        g_return_if_fail(priv->path);
        break;
    case PROP_DRIVER:
        /* construct-only */
        priv->driver = g_value_dup_string(value);
        break;
    case PROP_CONTROL_PORT:
        /* construct-only */
        priv->control_port = g_value_dup_string(value);
        break;
    case PROP_UID:
        /* construct-only */
        priv->uid = g_value_dup_string(value);
        break;
    case PROP_STATE:
        /* construct-only */
        priv->state = g_value_get_int(value);
        break;
    case PROP_DEVICE_ID:
        /* construct-only */
        priv->device_id = g_value_dup_string(value);
        break;
    case PROP_SIM_ID:
        g_free(priv->sim_id);
        priv->sim_id = g_value_dup_string(value);
        break;
    case PROP_IP_TYPES:
        priv->ip_types = g_value_get_uint(value);
        break;
    case PROP_SIM_OPERATOR_ID:
        nm_clear_g_free(&priv->sim_operator_id);
        s = g_value_get_string(value);
        if (s && s[0])
            priv->sim_operator_id = g_strdup(s);
        break;
    case PROP_OPERATOR_CODE:
        /* construct-only */
        priv->operator_code = g_value_dup_string(value);
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
        break;
    }
}

/*****************************************************************************/

static void
nm_modem_init(NMModem *self)
{
    NMModemPrivate *priv;

    self->_priv = G_TYPE_INSTANCE_GET_PRIVATE(self, NM_TYPE_MODEM, NMModemPrivate);
    priv        = self->_priv;

    priv->ip_ifindex       = -1;
    priv->ip4_route_table  = RT_TABLE_MAIN;
    priv->ip4_route_metric = 700;
    priv->ip6_route_table  = RT_TABLE_MAIN;
    priv->ip6_route_metric = 700;
}

static void
constructed(GObject *object)
{
    NMModemPrivate *priv;

    G_OBJECT_CLASS(nm_modem_parent_class)->constructed(object);

    priv = NM_MODEM_GET_PRIVATE(NM_MODEM(object));

    g_return_if_fail(priv->control_port);
}

/*****************************************************************************/

static void
dispose(GObject *object)
{
    NMModemPrivate *priv = NM_MODEM_GET_PRIVATE(object);

    g_clear_object(&priv->act_request);

    G_OBJECT_CLASS(nm_modem_parent_class)->dispose(object);
}

static void
finalize(GObject *object)
{
    NMModemPrivate *priv = NM_MODEM_GET_PRIVATE(object);

    g_free(priv->uid);
    g_free(priv->path);
    g_free(priv->driver);
    g_free(priv->control_port);
    g_free(priv->data_port);
    g_free(priv->ip_iface);
    g_free(priv->device_id);
    g_free(priv->sim_id);
    g_free(priv->sim_operator_id);
    g_free(priv->operator_code);
    g_free(priv->apn);

    G_OBJECT_CLASS(nm_modem_parent_class)->finalize(object);
}

static void
nm_modem_class_init(NMModemClass *klass)
{
    GObjectClass *object_class = G_OBJECT_CLASS(klass);

    g_type_class_add_private(object_class, sizeof(NMModemPrivate));

    object_class->constructed  = constructed;
    object_class->set_property = set_property;
    object_class->get_property = get_property;
    object_class->dispose      = dispose;
    object_class->finalize     = finalize;

    klass->modem_act_stage1_prepare  = modem_act_stage1_prepare;
    klass->stage3_ip6_config_request = stage3_ip6_config_request;
    klass->deactivate_cleanup        = deactivate_cleanup;

    obj_properties[PROP_UID] =
        g_param_spec_string(NM_MODEM_UID,
                            "",
                            "",
                            NULL,
                            G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);

    obj_properties[PROP_PATH] =
        g_param_spec_string(NM_MODEM_PATH,
                            "",
                            "",
                            NULL,
                            G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);

    obj_properties[PROP_DRIVER] =
        g_param_spec_string(NM_MODEM_DRIVER,
                            "",
                            "",
                            NULL,
                            G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);

    obj_properties[PROP_CONTROL_PORT] =
        g_param_spec_string(NM_MODEM_CONTROL_PORT,
                            "",
                            "",
                            NULL,
                            G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);

    obj_properties[PROP_IP_IFINDEX] = g_param_spec_int(NM_MODEM_IP_IFINDEX,
                                                       "",
                                                       "",
                                                       0,
                                                       G_MAXINT,
                                                       0,
                                                       G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

    obj_properties[PROP_STATE] =
        g_param_spec_int(NM_MODEM_STATE,
                         "",
                         "",
                         NM_MODEM_STATE_UNKNOWN,
                         _NM_MODEM_STATE_LAST,
                         NM_MODEM_STATE_UNKNOWN,
                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);

    obj_properties[PROP_DEVICE_ID] =
        g_param_spec_string(NM_MODEM_DEVICE_ID,
                            "",
                            "",
                            NULL,
                            G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);

    obj_properties[PROP_SIM_ID] =
        g_param_spec_string(NM_MODEM_SIM_ID,
                            "",
                            "",
                            NULL,
                            G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);

    obj_properties[PROP_IP_TYPES] =
        g_param_spec_uint(NM_MODEM_IP_TYPES,
                          "IP Types",
                          "Supported IP types",
                          0,
                          G_MAXUINT32,
                          NM_MODEM_IP_TYPE_IPV4,
                          G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);

    obj_properties[PROP_SIM_OPERATOR_ID] =
        g_param_spec_string(NM_MODEM_SIM_OPERATOR_ID,
                            "",
                            "",
                            NULL,
                            G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);

    obj_properties[PROP_OPERATOR_CODE] =
        g_param_spec_string(NM_MODEM_OPERATOR_CODE,
                            "",
                            "",
                            NULL,
                            G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);

    obj_properties[PROP_APN] =
        g_param_spec_string(NM_MODEM_APN, "", "", NULL, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

    g_object_class_install_properties(object_class, _PROPERTY_ENUMS_LAST, obj_properties);

    signals[PPP_STATS] = g_signal_new(NM_MODEM_PPP_STATS,
                                      G_OBJECT_CLASS_TYPE(object_class),
                                      G_SIGNAL_RUN_FIRST,
                                      0,
                                      NULL,
                                      NULL,
                                      NULL,
                                      G_TYPE_NONE,
                                      2,
                                      G_TYPE_UINT /*guint32 in_bytes*/,
                                      G_TYPE_UINT /*guint32 out_bytes*/);

    signals[PPP_FAILED] = g_signal_new(NM_MODEM_PPP_FAILED,
                                       G_OBJECT_CLASS_TYPE(object_class),
                                       G_SIGNAL_RUN_FIRST,
                                       0,
                                       NULL,
                                       NULL,
                                       NULL,
                                       G_TYPE_NONE,
                                       1,
                                       G_TYPE_UINT);

    signals[IP4_CONFIG_RESULT] = g_signal_new(NM_MODEM_IP4_CONFIG_RESULT,
                                              G_OBJECT_CLASS_TYPE(object_class),
                                              G_SIGNAL_RUN_FIRST,
                                              0,
                                              NULL,
                                              NULL,
                                              NULL,
                                              G_TYPE_NONE,
                                              2,
                                              G_TYPE_OBJECT,
                                              G_TYPE_POINTER);

    /**
     * NMModem::ip6-config-result:
     * @modem: the #NMModem  on which the signal is emitted
     * @config: the #NMIP6Config to apply to the modem's data port
     * @do_slaac: %TRUE if IPv6 SLAAC should be started
     * @error: a #GError if any error occurred during IP configuration
     *
     * This signal is emitted when IPv6 configuration has completed or failed.
     * If @error is set the configuration failed.  If @config is set, then
     * the details should be applied to the data port before any further
     * configuration (like SLAAC) is done.  @do_slaac indicates whether SLAAC
     * should be started after applying @config to the data port.
     */
    signals[IP6_CONFIG_RESULT] = g_signal_new(NM_MODEM_IP6_CONFIG_RESULT,
                                              G_OBJECT_CLASS_TYPE(object_class),
                                              G_SIGNAL_RUN_FIRST,
                                              0,
                                              NULL,
                                              NULL,
                                              NULL,
                                              G_TYPE_NONE,
                                              3,
                                              G_TYPE_OBJECT,
                                              G_TYPE_BOOLEAN,
                                              G_TYPE_POINTER);

    signals[PREPARE_RESULT] = g_signal_new(NM_MODEM_PREPARE_RESULT,
                                           G_OBJECT_CLASS_TYPE(object_class),
                                           G_SIGNAL_RUN_FIRST,
                                           0,
                                           NULL,
                                           NULL,
                                           NULL,
                                           G_TYPE_NONE,
                                           2,
                                           G_TYPE_BOOLEAN,
                                           G_TYPE_UINT);

    signals[AUTH_REQUESTED] = g_signal_new(NM_MODEM_AUTH_REQUESTED,
                                           G_OBJECT_CLASS_TYPE(object_class),
                                           G_SIGNAL_RUN_FIRST,
                                           0,
                                           NULL,
                                           NULL,
                                           NULL,
                                           G_TYPE_NONE,
                                           0);

    signals[AUTH_RESULT] = g_signal_new(NM_MODEM_AUTH_RESULT,
                                        G_OBJECT_CLASS_TYPE(object_class),
                                        G_SIGNAL_RUN_FIRST,
                                        0,
                                        NULL,
                                        NULL,
                                        NULL,
                                        G_TYPE_NONE,
                                        1,
                                        G_TYPE_POINTER);

    signals[REMOVED] = g_signal_new(NM_MODEM_REMOVED,
                                    G_OBJECT_CLASS_TYPE(object_class),
                                    G_SIGNAL_RUN_FIRST,
                                    0,
                                    NULL,
                                    NULL,
                                    NULL,
                                    G_TYPE_NONE,
                                    0);

    signals[STATE_CHANGED] = g_signal_new(NM_MODEM_STATE_CHANGED,
                                          G_OBJECT_CLASS_TYPE(object_class),
                                          G_SIGNAL_RUN_FIRST,
                                          0,
                                          NULL,
                                          NULL,
                                          NULL,
                                          G_TYPE_NONE,
                                          2,
                                          G_TYPE_INT,
                                          G_TYPE_INT);
}
